package net.geant.gembus.camel;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.TreeMap;

import net.geant.gembus.registry_publisher.RegistryPublishService;

import org.apache.camel.CamelContext;
import org.apache.camel.Endpoint;
import org.apache.camel.Route;
import org.apache.camel.component.jetty.JettyHttpEndpoint;
import org.apache.camel.component.restlet.RestletEndpoint;
import org.apache.camel.spi.Registry;
import org.apache.log4j.Logger;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.springframework.core.io.Resource;

/**
 * This class acts like a Service Listener in the OSGi container, listening for registrations of new CamelContexts and reacting to them.
 * @author raul
 */
public class CamelContextListener {
	
	private static final Logger LOG = Logger.getLogger(CamelContextListener.class);
	private static final int PUBLISH_OPERATION = 1;
	private static final int REMOVE_OPERATION = 2;
	
	private RegistryPublishService registryService;
	private BundleContext bundleContext;
	
	/**
	 * Introspect the Camel Context that has been registered, extracting useful meta information to publish it to the registry
	 * @param context
	 * @throws IOException
	 * @throws InvalidSyntaxException
	 */
	public void camelRegistered(CamelContext context) throws Exception {
		// CXF services exposed by Camel are intercepted by the CXFBusListener already ... so no need to do any special work here
		// Instead, this tracker should pivot around HTTP and Jetty endpoints, Restlet endpoints, etc. In essence, any other Camel endpoint that can
		// expose an HTTP service
		List<Route> routes = context.getRoutes();
		for (Route route : routes) {
			LOG.info("Registering route " + route.getId() + " in context " + context.getName() + " in GEMBus Registry");
			Endpoint consumer = route.getConsumer().getEndpoint();
			LOG.info("Consumer endpoint detected: " + consumer.getEndpointUri() + " of type: " + consumer.getClass());
			processEndpoint(consumer, context.getName(), route.getId(), context.getRegistry(), CamelContextListener.PUBLISH_OPERATION);
		}
	}
	
	public void camelUnregistered(CamelContext context) throws Exception {
		if (context == null) {
			return;
		}
		// unregister all routes
		List<Route> routes = context.getRoutes();
		for (Route route : routes) {
			LOG.info("Unregistering route " + route.getId() + " in context " + context.getName() + " in GEMBus Registry");
			Endpoint consumer = route.getConsumer().getEndpoint();
			LOG.info("Consumer endpoint detected: " + consumer.getEndpointUri() + " of type: " + consumer.getClass() + " => Deregistering");
			processEndpoint(consumer, context.getName(), route.getId(), context.getRegistry(), CamelContextListener.REMOVE_OPERATION);
		}
	}

	/**
	 * (IMPORTANT)
	 * 
	 * Enrolls the consumer endpoint in the GEMBus Registry - modify to suit the needs.
	 * 
	 * Right now it will:
	 *   a) publish all any resources (namely WADLs or could also be USDL) referenced inside a Map with id="serviceProperties" in the same
	 *      Spring or Blueprint registry as the CamelContext, via PUTs
	 *   b) POST all endpoint properties as service metadata (this is just for demo purposes - there is no way you would indeed want to do this in reality)
	 *   
	 * I have created a "convention" by which the service bundle being introspected can define a special map of type Map<String, Resource> called "serviceProperties" 
	 * inside its Blueprint or Spring Bean Registry. If this Map exists, the listener will match each discovered camelContext/routeId against the Map.
	 * If it finds a Resource, it will push its content to the Registry as if it was a WSDL, WADL or any service descriptor that the GEMBus Registry supports.
	 * 
	 * @param endpoint
	 * @param contextId
	 * @param routeId
	 * @param registry 
	 * @param operation 
	 * @throws Exception
	 */
	@SuppressWarnings("unchecked")
	private void processEndpoint(Endpoint endpoint, String contextId, String routeId, Registry registry, int operation) throws Exception {
		if (endpoint instanceof RestletEndpoint || endpoint instanceof JettyHttpEndpoint) {
			String serviceId = contextId + "/" + routeId;
			if (operation == CamelContextListener.PUBLISH_OPERATION) {
				Map<String, Resource> map = (Map<String, Resource>) registry.lookup("serviceProperties", Map.class);
				Resource res = map.get(serviceId);
				if (res != null) {
					String resource = new Scanner(res.getInputStream()).useDelimiter("\\A").next();
					registryService.publishService(serviceId, resource);
				}
	
				// TODO: define what would be the service name for services not defined by a WSDL (those who are defined by a WSDL will be CXF services and picked
				// up by the CXFBusListener instead) -- for the time being, use the concatenation camelContextId + "/" + routeId
				Map<String, String> endpointProperties = getNonNullProperties(endpoint);
				registryService.updateMetadata(serviceId, endpointProperties);
			} else if (operation == CamelContextListener.REMOVE_OPERATION) {
				registryService.removeService(serviceId);
			}
		}
	}
	
	public BundleContext getBundleContext() {
		return bundleContext;
	}

	public void setBundleContext(BundleContext bundleContext) {
		this.bundleContext = bundleContext;
	}

	/**
	 * Utility method to introspect a JavaBean and extract a HashMap of key=>value of its properties
	 * @param input
	 * @return
	 */
	private Map<String, String> getNonNullProperties(final Object input) {
	    final Map<String, String> nonNullProperties = new TreeMap<String, String>();
	    try {
	        final BeanInfo beanInfo = Introspector.getBeanInfo(input.getClass());
	        for (final PropertyDescriptor descriptor : beanInfo.getPropertyDescriptors()) {
	            try {
	            	if (descriptor.getReadMethod() == null) {
	            		continue;
	            	}
	                final Object propertyValue = descriptor.getReadMethod().invoke(input);
	                if (propertyValue != null) {
	                    nonNullProperties.put(descriptor.getName(), propertyValue.toString());
	                }
	            } catch (final Exception e) {
	            	return null;
	            }
	        }
	    } catch (Exception e) {
	        return null;
	    }
	    return nonNullProperties;
	}

	public RegistryPublishService getRegistryService() {
		return registryService;
	}

	public void setRegistryService(RegistryPublishService registryService) {
		this.registryService = registryService;
	}
	
}
